<template>
  <div>
    <h1>WebRTC (Web Real-Time Communications) 演示</h1>

    <br />

    <form>
      <label>
        音频输入设备:
        <select
          v-model="audioInput.selected"
          @change="handleChangeOfAudioInput"
        >
          <option disabled value="">请选择</option>
          <option v-for="option in audioInput.options" :value="option.value">
            {{ option.label }}
          </option>
        </select>
      </label>

      <label>
        视频输入设备:
        <select
          v-model="videoInput.selected"
          @change="handleChangeOfVideoInput"
        >
          <option disabled value="">请选择</option>
          <option v-for="option in videoInput.options" :value="option.value">
            {{ option.label }}
          </option>
        </select>
      </label>

      <label>
        音频输出设备:
        <select
          v-model="audioOutput.selected"
          @change="handleChangeOfAudioOutput"
        >
          <option disabled value="">请选择</option>
          <option v-for="option in audioOutput.options" :value="option.value">
            {{ option.label }}
          </option>
        </select>
      </label>
    </form>

    <br />

    <div>
      <button :disabled="buttons.startRecord" @click="startRecord">
        开始录制
      </button>
      <button :disabled="buttons.pauseRecord" @click="pauseRecord">
        暂停录制
      </button>
      <button :disabled="buttons.resumeRecord" @click="resumeRecord">
        恢复录制
      </button>
      <button :disabled="buttons.stopRecord" @click="stopRecord">
        结束录制
      </button>
      <button :disabled="buttons.recPlay" @click="recPlay">回放录制文件</button>
      <button :disabled="buttons.downloadRecord" @click="downloadRecord">
        下载录制文件
      </button>

      <br />
      <br />

      <label>
        滤镜:
        <select v-model="photoFilter" @change="handleChangeOfPhotoFilter">
          <option disabled value="">请选择</option>
          <option value="none">无</option>
          <option value="blur">模糊(毛玻璃)</option>
          <option value="grayscale">黑白</option>
          <option value="invert">反转颜色</option>
          <option value="sepia">棕褐色</option>
        </select>
      </label>

      <button :disabled="buttons.takePhoto" @click="takePhoto">拍照</button>
      <button :disabled="buttons.savePhoto" @click="savePhoto">保存照片</button>

      <br />
      <br />

      <video ref="player" autoplay playsinline>
        您的浏览器不支持视频播放！
      </video>

      <video ref="recplayer">您的浏览器不支持视频播放！</video>

      <canvas
        ref="photo"
        :width="canvas.width"
        :height="canvas.height"
        :class="canvas.class"
      >
        <p>您的浏览器不支持Canvas标签！</p>
      </canvas>
    </div>

    <br />
    <br />

    <div>
      <button :disabled="desktopButtons.shareDesktop" @click="shareDesktop">
        共享桌面（屏幕）
      </button>
      <button
        :disabled="desktopButtons.startRecordDesktop"
        @click="startRecordDesktop"
      >
        开始录制桌面
      </button>
      <button
        :disabled="desktopButtons.pauseRecordDesktop"
        @click="pauseRecordDesktop"
      >
        暂停录制桌面
      </button>
      <button
        :disabled="desktopButtons.resumeRecordDesktop"
        @click="resumeRecordDesktop"
      >
        恢复录制桌面
      </button>
      <button
        :disabled="desktopButtons.stopRecordDesktop"
        @click="stopRecordDesktop"
      >
        结束录制桌面
      </button>
      <button :disabled="desktopButtons.recPlayDesktop" @click="recPlayDesktop">
        回放录制桌面
      </button>
      <button
        :disabled="desktopButtons.downloadRecordDesktop"
        @click="downloadRecordDesktop"
      >
        下载录制桌面
      </button>

      <br />
      <br />

      <label>
        滤镜:
        <select v-model="desktopFilter" @change="handleChangeOfDesktopFilter">
          <option disabled value="">请选择</option>
          <option value="none">无</option>
          <option value="blur">模糊(毛玻璃)</option>
          <option value="grayscale">黑白</option>
          <option value="invert">反转颜色</option>
          <option value="sepia">棕褐色</option>
        </select>
      </label>

      <button :disabled="desktopButtons.takeDesktop" @click="takeDesktop">
        截取共享桌面（屏幕）
      </button>
      <button :disabled="desktopButtons.saveDesktop" @click="saveDesktop">
        保存共享桌面（屏幕）
      </button>

      <br />
      <br />

      <video ref="desktopPlayer" autoplay playsinline>
        您的浏览器不支持视频播放！
      </video>

      <video ref="desktopRecplayer">您的浏览器不支持视频播放！</video>

      <canvas
        ref="desktop"
        :width="canvas.width"
        :height="canvas.height"
        :class="canvas.class"
      >
        <p>您的浏览器不支持Canvas标签！</p>
      </canvas>
    </div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";

type Select = {
  options: SelectOption[];
  selected: string;
};

type SelectOption = {
  value: string;
  label: string;
};

const player = ref<HTMLVideoElement>();
const photo = ref<HTMLCanvasElement>();
const recplayer = ref<HTMLVideoElement>();

const mediaRecorder = ref<MediaRecorder>();
const recordBuffer = ref<any[]>([]);

const mediaStreamConstraints: MediaStreamConstraints = reactive({
  video: {
    frameRate: { min: 20 },
    width: { min: 640, ideal: 1280 },
    height: { min: 360, ideal: 720 },
    aspectRatio: 16 / 9,
    facingMode: "environment", // user:前置摄像头，environment:后置摄像头
  },
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true,
  },
});

const photoFilter = ref("");

const buttons = reactive({
  takePhoto: true,
  savePhoto: true,
  startRecord: true,
  pauseRecord: true,
  resumeRecord: true,
  stopRecord: true,
  recPlay: true,
  downloadRecord: true,
});

const desktopPlayer = ref<HTMLVideoElement>();
const desktop = ref<HTMLCanvasElement>();
const desktopRecplayer = ref<HTMLVideoElement>();

const desktopMediaRecorder = ref<MediaRecorder>();
const desktopRecordBuffer = ref<any[]>([]);

// 桌面采集的参数里不能对音频进行限制，即不能在采集桌面的同时采集音频
const desktopMediaStreamConstraints: MediaStreamConstraints = reactive({
  video: {
    frameRate: 20,
    width: 1280,
    height: 720,
  },
});

const desktopFilter = ref("");

const desktopButtons = reactive({
  takeDesktop: true,
  saveDesktop: true,
  shareDesktop: false,
  startRecordDesktop: true,
  pauseRecordDesktop: true,
  resumeRecordDesktop: true,
  stopRecordDesktop: true,
  recPlayDesktop: true,
  downloadRecordDesktop: true,
});

const audioInput = reactive<Select>({
  options: [],
  selected: "",
});

const videoInput = reactive<Select>({
  options: [],
  selected: "",
});

const audioOutput = reactive<Select>({
  options: [],
  selected: "",
});

const canvas = reactive({
  width: 1280,
  height: 720,
  class: "",
});

onMounted(() => {
  // 判断浏览器是否支持这些 API
  if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
    console.log("enumerateDevices() not supported.");
    return;
  } else {
    initDevices();
    collect(mediaStreamConstraints);
  }
});

function initDevices() {
  // 枚举 cameras 和 microphones
  navigator.mediaDevices
    .enumerateDevices()
    .then(function (devices) {
      const audioInputOptions: SelectOption[] = [];
      const videoInputOptions: SelectOption[] = [];
      const audioOutputOptions: SelectOption[] = [];

      // 设备信息
      devices.forEach(function (deviceInfo) {
        console.log(
          `${deviceInfo.kind}: ${deviceInfo.label}  id = ${deviceInfo.deviceId}`
        );

        if (deviceInfo.kind === "audioinput") {
          audioInputOptions.push({
            value: deviceInfo.deviceId,
            label: deviceInfo.label,
          });
        } else if (deviceInfo.kind === "videoinput") {
          videoInputOptions.push({
            value: deviceInfo.deviceId,
            label: deviceInfo.label,
          });
        } else if (deviceInfo.kind === "audiooutput") {
          audioOutputOptions.push({
            value: deviceInfo.deviceId,
            label: deviceInfo.label,
          });
        }
      });

      audioInput.options = audioInputOptions;

      videoInput.options = videoInputOptions;

      audioOutput.options = audioOutputOptions;
    })
    .catch(function (err) {
      console.log(err.name + ": " + err.message);
    });
}

function handleChangeOfAudioInput(event: Event) {}

function handleChangeOfVideoInput(event: Event) {}

function handleChangeOfAudioOutput(event: Event) {}

function handleChangeOfPhotoFilter(event: Event) {}

function handleChangeOfDesktopFilter(event: Event) {}

function download(url: string, fileName: string) {
  const oA = document.createElement("a");
  oA.download = fileName; // 设置下载的文件名，默认是'下载'
  oA.href = url;
  oA.style.display = "none";
  document.body.appendChild(oA);
  oA.click();
  oA.remove(); // 下载之后把创建的元素删除
}

/** 采集音视频数据 */
function collect(mediaStreamConstraints: MediaStreamConstraints) {
  navigator.mediaDevices
    .getUserMedia(mediaStreamConstraints)
    .then(gotLocalMediaStream)
    .catch(handleLocalMediaStreamError);
}

function gotLocalMediaStream(mediaStream: MediaStream) {
  player.value!.srcObject = mediaStream;
  buttons.startRecord = false;
  buttons.takePhoto = false;
}

function handleLocalMediaStreamError(error: Error) {
  console.log("navigator.getUserMedia error: ", error);
  console.log("mediaStreamConstraints: ", mediaStreamConstraints);
}

/** 初始化录制参数 */
function initRecord() {
  // 设置录制下来的多媒体格式
  const options = {
    mimeType: "video/webm;codecs=vp8",
  };

  // 判断浏览器是否支持录制
  if (!MediaRecorder.isTypeSupported(options.mimeType)) {
    console.error(`${options.mimeType} is not supported!`);
    return;
  }

  // 类型“HTMLVideoElement”上不存在属性“captureStream”。
  // https://www.qiniu.com/qfans/qnso-68044611
  const stream = (player.value as any).captureStream(30); // 录制帧率30FPS

  try {
    // 创建录制对象
    mediaRecorder.value = new MediaRecorder(stream, options);
  } catch (e) {
    console.error("Failed to create MediaRecorder:", e);
  }
}

/** 开始录制 */
function startRecord(event: Event) {
  recordBuffer.value = [];

  if (!mediaRecorder.value) {
    initRecord();
  }

  const mediaRecorderValue = mediaRecorder.value;

  if (mediaRecorderValue) {
    buttons.startRecord = true;
    buttons.pauseRecord = false;
    buttons.resumeRecord = true;
    buttons.stopRecord = false;
    buttons.recPlay = false;
    buttons.downloadRecord = false;

    // 当有音视频数据来了之后触发该事件
    mediaRecorderValue.ondataavailable = handleDataAvailable;
    // 开始录制
    mediaRecorderValue.start(10);
  }
}

/** 当该函数被触发后，将数据压入到 blob 中 */
function handleDataAvailable(e: BlobEvent) {
  if (e && e.data && e.data.size > 0) {
    recordBuffer.value.push(e.data);
  }
}

/** 暂停录制 */
function pauseRecord(event: Event) {
  const mediaRecorderValue = mediaRecorder.value;

  if (mediaRecorderValue) {
    buttons.pauseRecord = true;
    buttons.resumeRecord = false;

    mediaRecorderValue.pause();
  }
}

/** 恢复录制 */
function resumeRecord(event: Event) {
  const mediaRecorderValue = mediaRecorder.value;

  if (mediaRecorderValue) {
    buttons.pauseRecord = false;
    buttons.resumeRecord = true;

    mediaRecorderValue.resume();
  }
}

/** 结束录制 */
function stopRecord(event: Event) {
  const mediaRecorderValue = mediaRecorder.value;

  if (mediaRecorderValue) {
    buttons.startRecord = false;
    buttons.pauseRecord = true;
    buttons.resumeRecord = true;
    buttons.stopRecord = true;

    mediaRecorderValue.stop();
  }
}

/** 回放录制文件 */
function recPlay(event: Event) {
  const recordBufferValue = recordBuffer.value;

  if (!recordBufferValue) {
    alert("尚未开始录制");
    return;
  }

  const recplayerValue = recplayer.value;

  if (recplayerValue) {
    let blob = new Blob(recordBufferValue, { type: "video/webm" });
    recplayerValue.src = window.URL.createObjectURL(blob);
    recplayerValue.srcObject = null;
    recplayerValue.controls = true;
    recplayerValue.play();
  }
}

/** 下载录制文件 */
function downloadRecord(event: Event) {
  const recordBufferValue = recordBuffer.value;

  if (!recordBufferValue) {
    alert("尚未开始录制");
    return;
  }

  let blob = new Blob(recordBufferValue, { type: "video/webm" });
  let url = window.URL.createObjectURL(blob);

  download(url, "video.mp4");
}

/** 拍照 */
function takePhoto(event: Event) {
  const filter = photoFilter.value;
  const photoValue = photo.value;

  if (!photoValue) {
    console.error("photo.value is null");
    return;
  }

  const ctx = photoValue.getContext("2d");

  if (!ctx) {
    console.error("photo.value.getContext('2d') is null");
    return;
  }

  // https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/filter
  // 滤镜函数: https://developer.mozilla.org/zh-CN/docs/Web/CSS/filter-function

  if (filter === "blur") {
    ctx.filter = "blur(2px)";
  } else if (filter === "grayscale") {
    ctx.filter = "grayscale(1)";
  } else if (filter === "invert") {
    ctx.filter = "invert(1)";
  } else if (filter === "sepia") {
    ctx.filter = "sepia(1)";
  } else {
    ctx.filter = "none";
  }

  ctx.drawImage(player.value!, 0, 0, canvas.width, canvas.height);

  buttons.savePhoto = false;
}

/** 保存照片 */
function savePhoto(event: Event) {
  const photoValue = photo.value;

  if (!photoValue) {
    console.error("picture.value is null");
    return;
  }

  const url = photoValue.toDataURL(photoValue.toDataURL("image/jpeg"));
  download(url, "photo");
}

// =============================================================================

/** 共享桌面（屏幕） */
function shareDesktop(event: Event) {
  collectDesktop(desktopMediaStreamConstraints);
}

/** 采集桌面 */
function collectDesktop(desktopMediaStreamConstraints: MediaStreamConstraints) {
  navigator.mediaDevices
    .getDisplayMedia(desktopMediaStreamConstraints)
    .then(gotDesktopMediaStream)
    .catch(handleDesktopMediaStreamError);
}

function gotDesktopMediaStream(mediaStream: MediaStream) {
  desktopPlayer.value!.srcObject = mediaStream;

  desktopButtons.startRecordDesktop = false;
  desktopButtons.takeDesktop = false;
}

function handleDesktopMediaStreamError(error: Error) {
  console.log("navigator.getDisplayMedia error: ", error);
  console.log("desktopMediaStreamConstraints: ", desktopMediaStreamConstraints);
}

/** 初始化录制桌面参数 */
function initRecordDesktop() {
  // 设置录制下来的多媒体格式
  const options = {
    mimeType: "video/webm;codecs=vp8",
  };

  // 判断浏览器是否支持录制
  if (!MediaRecorder.isTypeSupported(options.mimeType)) {
    console.error(`${options.mimeType} is not supported!`);
    return;
  }

  // 类型“HTMLVideoElement”上不存在属性“captureStream”。
  // https://www.qiniu.com/qfans/qnso-68044611
  const stream = (desktopPlayer.value as any).captureStream(30); // 录制帧率30FPS

  try {
    // 创建录制对象
    desktopMediaRecorder.value = new MediaRecorder(stream, options);
  } catch (e) {
    console.error("Failed to create MediaRecorder:", e);
  }
}

/** 开始录制桌面 */
function startRecordDesktop(event: Event) {
  desktopRecordBuffer.value = [];

  if (!desktopMediaRecorder.value) {
    initRecordDesktop();
  }

  const mediaRecorderValue = desktopMediaRecorder.value;

  if (mediaRecorderValue) {
    desktopButtons.startRecordDesktop = true;
    desktopButtons.pauseRecordDesktop = false;
    desktopButtons.resumeRecordDesktop = true;
    desktopButtons.stopRecordDesktop = false;
    desktopButtons.recPlayDesktop = false;
    desktopButtons.downloadRecordDesktop = false;

    // 当有音视频数据来了之后触发该事件
    mediaRecorderValue.ondataavailable = handleDesktopDataAvailable;
    // 开始录制
    mediaRecorderValue.start(10);
  }
}

/** 当该函数被触发后，将数据压入到 blob 中 */
function handleDesktopDataAvailable(e: BlobEvent) {
  if (e && e.data && e.data.size > 0) {
    desktopRecordBuffer.value.push(e.data);
  }
}

/** 暂停录制桌面 */
function pauseRecordDesktop(event: Event) {
  const mediaRecorderValue = desktopMediaRecorder.value;

  if (mediaRecorderValue) {
    desktopButtons.pauseRecordDesktop = true;
    desktopButtons.resumeRecordDesktop = false;

    mediaRecorderValue.pause();
  }
}

/** 恢复录制桌面 */
function resumeRecordDesktop(event: Event) {
  const mediaRecorderValue = desktopMediaRecorder.value;

  if (mediaRecorderValue) {
    desktopButtons.pauseRecordDesktop = false;
    desktopButtons.resumeRecordDesktop = true;

    mediaRecorderValue.resume();
  }
}

/** 结束录制桌面 */
function stopRecordDesktop(event: Event) {
  const mediaRecorderValue = desktopMediaRecorder.value;

  if (mediaRecorderValue) {
    desktopButtons.startRecordDesktop = false;
    desktopButtons.pauseRecordDesktop = true;
    desktopButtons.resumeRecordDesktop = true;
    desktopButtons.stopRecordDesktop = true;

    mediaRecorderValue.stop();
  }
}

/** 回放录制桌面 */
function recPlayDesktop(event: Event) {
  const recordBufferValue = desktopRecordBuffer.value;

  if (!recordBufferValue) {
    alert("尚未开始录制");
    return;
  }

  const recplayerValue = desktopRecplayer.value;

  if (recplayerValue) {
    let blob = new Blob(recordBufferValue, { type: "video/webm" });
    recplayerValue.src = window.URL.createObjectURL(blob);
    recplayerValue.srcObject = null;
    recplayerValue.controls = true;
    recplayerValue.play();
  }
}

/** 下载录制桌面 */
function downloadRecordDesktop(event: Event) {
  const recordBufferValue = desktopRecordBuffer.value;

  if (!recordBufferValue) {
    alert("尚未开始录制");
    return;
  }

  let blob = new Blob(recordBufferValue, { type: "video/webm" });
  let url = window.URL.createObjectURL(blob);

  download(url, "desktop.mp4");
}

/** 截取共享桌面（屏幕） */
function takeDesktop(event: Event) {
  const filter = desktopFilter.value;
  const desktopValue = desktop.value;

  if (!desktopValue) {
    console.error("desktop.value is null");
    return;
  }

  const ctx = desktopValue.getContext("2d");

  if (!ctx) {
    console.error("desktop.value.getContext('2d') is null");
    return;
  }

  // https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/filter
  // 滤镜函数: https://developer.mozilla.org/zh-CN/docs/Web/CSS/filter-function

  if (filter === "blur") {
    ctx.filter = "blur(2px)";
  } else if (filter === "grayscale") {
    ctx.filter = "grayscale(1)";
  } else if (filter === "invert") {
    ctx.filter = "invert(1)";
  } else if (filter === "sepia") {
    ctx.filter = "sepia(1)";
  } else {
    ctx.filter = "none";
  }

  ctx.drawImage(desktopPlayer.value!, 0, 0, canvas.width, canvas.height);

  desktopButtons.saveDesktop = false;
}

/** 保存共享桌面（屏幕） */
function saveDesktop(event: Event) {
  const desktopValue = desktop.value;

  if (!desktopValue) {
    console.error("desktop.value is null");
    return;
  }

  const url = desktopValue.toDataURL(desktopValue.toDataURL("image/jpeg"));
  download(url, "desktop");
}
</script>

<style scoped>
.none {
  -webkit-filter: none; /* Chrome, Safari, Opera */
  filter: none;
}

.blur {
  -webkit-filter: blur(3px); /* Chrome, Safari, Opera */
  filter: blur(3px);
}

.grayscale {
  -webkit-filter: grayscale(1); /* Chrome, Safari, Opera */
  filter: grayscale(1);
}

.invert {
  -webkit-filter: invert(1); /* Chrome, Safari, Opera */
  filter: invert(1);
}

.sepia {
  -webkit-filter: sepia(1); /* Chrome, Safari, Opera */
  filter: sepia(1);
}
</style>
